001    /*
002     * Copyright 2004-2005 Stephen J. McConnell.
003     * Copyright 2004 Niclas Hedhman.
004     *
005     * Licensed  under the  Apache License,  Version 2.0  (the "License");
006     * you may not use  this file  except in  compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *   http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed  under the  License is distributed on an "AS IS" BASIS,
013     * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
014     * implied.
015     *
016     * See the License for the specific language governing permissions and
017     * limitations under the License.
018     */
019    
020    package net.dpml.transit;
021    
022    import java.io.Serializable;
023    import java.net.URLStreamHandler;
024    
025    import java.net.MalformedURLException;
026    import java.net.URI;
027    import java.net.URISyntaxException;
028    import java.net.URL;
029    
030    /**
031     * A utility class the handles validation of <code>artifact</code> style uri
032     * strings.
033     *
034     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
035     * @version 1.0.1
036     */
037    public final class Artifact implements Serializable, Comparable
038    {
039       /**
040        * Constant scheme name for the artifact protocol.
041        */
042        public static final String ARTIFACT = "artifact";
043        
044       /**
045        * Constant scheme name for the link protocol.
046        */
047        public static final String LINK = "link";
048        
049       /**
050        * Constant scheme name for the local protocol.
051        */
052        public static final String LOCAL = "local";
053        
054        static final long serialVersionUID = 1L;
055        
056        // ------------------------------------------------------------------------
057        // static
058        // ------------------------------------------------------------------------
059    
060        /**
061         * Creation of a new artifact instance using a supplied uri specification.
062         * An artifact uri contains the protocol identifier, a type, a group
063         * designator, a name, and an optional version identifier.
064         * <p>The following represent valid artifact uri examples:</p>
065         *
066         * <ul>
067         * <li>artifact:jar:dpml/transit/dpml-transit-main#1234</li>
068         * <li>artifact:jar:dpml/transit/dpml-transit-main</li>
069         * <li>link:jar:dpml/transit/dpml-transit-main#1.0</li>
070         * </ul>
071         *
072         * <p>
073         *  If there is a internal reference identifier which is marked by the
074         *  exclamation mark followed by slash (!/) it will be stripped. The
075         *  version part can be either before or after such identifier. Example;
076         *  <pre>
077         *   artifact:war:jmx-html/jmx-html#1.3!/images/abc.png
078         *   artifact:war:jmx-html/jmx-html!/images/abc.png#1.3
079         *  </pre>
080         *  The above uris will both be referencing
081         *  <code>artifact:war:jmx-html/jmx-html#1.3</code>
082         * </p>
083         * @param uri the artifact uri
084         * @return the new artifact
085         * @exception java.net.URISyntaxException if the supplied uri is not valid.
086         * @exception UnsupportedSchemeException if the URI does not have "artifact"
087         *         or "link" as its <strong>scheme</strong>.
088         */
089        public static final Artifact createArtifact( String uri )
090            throws URISyntaxException, UnsupportedSchemeException
091        {
092            if( null == uri )
093            {
094                throw new NullArgumentException( "uri" );
095            }
096            int asterix = uri.indexOf( "!" );
097            if( asterix == -1 )
098            {
099                return createArtifact( new URI( uri ) );
100            }
101            else
102            {
103                String path = uri.substring( 0, asterix );
104                int versionPos = uri.indexOf( "#" );
105                if( versionPos < asterix )
106                {
107                    return createArtifact( path );
108                }
109                else
110                {
111                    path = path + uri.substring( versionPos );
112                    return createArtifact( path );
113                }
114            }
115        }
116    
117        /**
118         * Creation of a new artifact instance using a supplied uri specification. An
119         * artifact uri contains the protocol identifier, an optional type, a group
120         * designator, a name, and an optional version identifier.
121         * <p>The following represent valid artifact uri examples:</p>
122         *
123         * <ul>
124         * <li>artifact:jar:metro/cache/dpml-cache-main#1.0.0</li>
125         * <li>artifact:metro/cache/dpml-cache-main#1.0.0</li>
126         * <li>artifact:metro/cache/dpml-cache-main</li>
127         * </ul>
128         *
129         * @param uri the artifact uri
130         * @return the new artifact
131         * @exception UnsupportedSchemeException if the URI does not have "artifact"
132         *     or "link" as its <strong>scheme</strong>.
133         */
134        public static final Artifact createArtifact( URI uri )
135            throws UnsupportedSchemeException
136        {
137            if( null == uri )
138            {
139                throw new NullArgumentException( "uri" );
140            }
141            String scheme = uri.getScheme();
142            if( null == scheme )
143            {
144                final String error = 
145                  "URI does not declare a scheme: " + uri;
146                throw new UnsupportedSchemeException( error );
147            }
148            //if( !scheme.equals( ARTIFACT ) && !scheme.equals( LINK ) && !scheme.equals( LOCAL ) )
149            //{
150            //    final String error = 
151            //      "URI contains a scheme that is not recognized: " + uri;
152            //    throw new UnsupportedSchemeException( error );
153            //}
154            return new Artifact( uri );
155        }
156    
157        /**
158         * Creation of a new artifact instance using a supplied group, name,
159         * version and type arguments.
160         *
161         * @param group the artifact group identifier
162         * @param name the artifact name
163         * @param version the version
164         * @param type the type
165         * @return the new artifact
166         * @exception NullArgumentException if any of the <code>group</code>,
167         *            <code>name</code> or <code>type</code> arguments are
168         *            <code>null</code>.
169         */
170        public static Artifact createArtifact( String group, String name, String version, String type )
171            throws NullArgumentException
172        {
173            return createArtifact( ARTIFACT, group, name, version, type );
174        }
175        
176        /**
177         * Creation of a new artifact instance using a supplied group, name,
178         * version and type arguments.
179         *
180         * @param scheme the artifact scheme
181         * @param group the artifact group identifier
182         * @param name the artifact name
183         * @param version the version
184         * @param type the type
185         * @return the new artifact
186         * @exception NullArgumentException if any of the <code>group</code>,
187         *            <code>name</code> or <code>type</code> arguments are
188         *            <code>null</code>.
189         */
190        public static Artifact createArtifact( String scheme, String group, String name, String version, String type )
191            throws NullArgumentException
192        {
193            if( name == null )
194            {
195                throw new NullArgumentException( "name" );
196            }
197            if( type == null )
198            {
199                throw new NullArgumentException( "type" );
200            }
201            if( scheme == null )
202            {
203                throw new NullArgumentException( "scheme" );
204            }
205            String composite = buildComposite( scheme, group, name, version, type );
206            try
207            {
208                URI uri = new URI( composite );
209                return new Artifact( uri );
210            } 
211            catch( URISyntaxException e )
212            {
213                // Can not happen.
214                final String error =
215                  "An internal error has occurred. "
216                  + "The following URI could not be constructed: " + composite;
217                throw new TransitRuntimeException( error );
218            }
219        }
220        
221        private static String buildComposite( String scheme, String group, String name, String version, String type )
222        {
223            if( null == group )
224            {
225                if( null == version )
226                {
227                    return scheme + ":" + type + ":" + name;
228                }
229                else
230                {
231                    return scheme + ":" + type + ":" + name + "#" + version;
232                }
233            }
234            else
235            {
236                if( null == version )
237                {
238                    return scheme + ":" + type + ":" + group + "/" + name;
239                }
240                else
241                {
242                    return scheme + ":" + type + ":" + group + "/" + name + "#" + version;
243                }
244            }
245        }
246        
247       /**
248        * Construct a new URL form a given URI.  If the URI is a Transit URI the 
249        * returned URL will be associated with the appropriate handler.
250        * @param uri the uri to convert
251        * @return the converted url
252        * @exception MalformedURLException if the url could not be created
253        */
254        public static URL toURL( URI uri ) throws MalformedURLException
255        {
256            try
257            {
258                Artifact artifact = Artifact.createArtifact( uri );
259                return artifact.toURL();
260            }
261            catch( UnsupportedSchemeException e )
262            {
263            }
264            catch( IllegalArgumentException e )
265            {
266            }
267            
268            try
269            {
270                return uri.toURL();
271            }
272            catch( MalformedURLException mue )
273            {
274                throw mue;
275            }
276            catch( Throwable t )
277            {
278                final String error = 
279                  "Unexpected error while attempting to convert a uri to a url."
280                  + "\n  URI: " 
281                  + uri;
282                throw new TransitRuntimeException( error, t );
283            }
284        }
285    
286       /**
287        * Test if the supplied uri is from the artifact family.  Specificially
288        * the test validates that the supplied uri has a scheme corresponding to 
289        * 'artifact', link', or 'local'.
290        * @param uri the uri to check
291        * @return true if thie uri is artifact based
292        */
293        public static boolean isRecognized( URI uri )
294        {
295            String scheme = uri.getScheme();
296            if( ARTIFACT.equals( scheme ) )
297            {
298                return true;
299            }
300            else if( LINK.equals( scheme ) )
301            {
302                return true;
303            }
304            else
305            {
306                return LOCAL.equals( scheme );
307            }
308        }
309    
310        // ------------------------------------------------------------------------
311        // state
312        // ------------------------------------------------------------------------
313    
314        /**
315         * The artifact uri.
316         */
317        private final URI m_uri;
318    
319        /**
320         * The artifact group.
321         */
322        private final String m_group;
323    
324        /**
325         * The artifact name.
326         */
327        private final String m_name;
328    
329        /**
330         * The artifact type.
331         */
332        private final String m_type;
333    
334        // ------------------------------------------------------------------------
335        // constructor
336        // ------------------------------------------------------------------------
337    
338        /**
339         * Creation of a new Artifact using a supplied uri.
340         * @param uri a uri of the form [scheme]:[type]:[group]/[name]#[version]
341         *   where [scheme] is one of 'link', 'artifact' or 'local'.
342         */
343        private Artifact( URI uri )
344            throws IllegalArgumentException
345        {
346            m_uri = uri;
347            String ssp = uri.getSchemeSpecificPart();
348    
349            if( ssp.indexOf( "//" ) > -1
350              || ssp.indexOf( ":/" ) > -1
351              || ssp.endsWith( "/" ) )
352            {
353                final String error =
354                  "Invalid character sequence in uri ["
355                  + uri + "].";
356                throw new IllegalArgumentException( error );
357            }
358            
359            int typeIndex = ssp.indexOf( ':' );
360            if( typeIndex > -1 )
361            {
362                String type = ssp.substring( 0, typeIndex );
363                m_type = type;
364                ssp = ssp.substring( typeIndex + 1 );
365            }
366            else
367            {
368                final String error = "Supplied artifact specification ["
369                  + uri + "] does not contain a type.";
370                throw new IllegalArgumentException( error );
371            }
372            
373            // ssp now contains group, name and version
374            
375            int groupIndex = ssp.lastIndexOf( '/' );
376            if( groupIndex > -1 )
377            {
378                String group = ssp.substring( 0, groupIndex );
379                m_group = group;
380                m_name = ssp.substring( groupIndex + 1 );
381            }
382            else
383            {
384                m_group = null;
385                m_name = ssp;
386            }
387            
388            String ver = uri.getFragment();
389            if( ver != null )
390            {
391                if( ver.indexOf( '/' ) >= 0
392                  || ver.indexOf( '%' ) >= 0
393                  || ver.indexOf( '\\' ) >= 0
394                  || ver.indexOf( '*' ) >= 0
395                  || ver.indexOf( '!' ) >= 0
396                  || ver.indexOf( '(' ) >= 0
397                  || ver.indexOf( '@' ) >= 0
398                  || ver.indexOf( ')' ) >= 0
399                  || ver.indexOf( '+' ) >= 0
400                  || ver.indexOf( '\'' ) >= 0
401                  || ver.indexOf( '{' ) >= 0
402                  || ver.indexOf( '}' ) >= 0
403                  || ver.indexOf( '[' ) >= 0
404                  || ver.indexOf( '}' ) >= 0
405                  || ver.indexOf( '?' ) >= 0
406                  || ver.indexOf( ',' ) >= 0
407                  || ver.indexOf( '#' ) >= 0
408                  || ver.indexOf( '=' ) >= 0
409                )
410                {
411                    final String error =
412                      "Supplied artifact specification ["
413                        + uri
414                        + "] contains illegal characters in the Version part.";
415                    throw new IllegalArgumentException( error );
416                }
417            }
418        }
419    
420        // ------------------------------------------------------------------------
421        // public
422        // ------------------------------------------------------------------------
423    
424        /**
425         * Return the protocol for the artifact.
426         *
427         * @return the protocol scheme
428         */
429        public final String getScheme()
430        {
431            return m_uri.getScheme();
432        }
433    
434        /**
435         * Return the group identifier for the artifact.  The group identifier
436         * is composed of a sequence of named separated by the '/' character.
437         *
438         * @return the group identifier
439         */
440        public final String getGroup()
441        {
442            return m_group;
443        }
444    
445        /**
446         * Return the name of the artifact.
447         *
448         * @return the artifact name
449         */
450        public final String getName()
451        {
452            return m_name;
453        }
454    
455        /**
456         * Return the type of the artifact.
457         *
458         * @return the artifact type
459         */
460        public final String getType()
461        {
462            return m_type;
463        }
464    
465        /**
466         * Return the posssibly null version identifier.  The value of the version
467         * is an opaque string.
468         * @return the artifact version
469         */
470        public final String getVersion()
471        {
472            String ver = m_uri.getFragment();
473            if( ver == null )
474            {
475                return null;
476            }
477            else if( ver.length() == 0 )
478            {
479                return null;
480            }
481            else
482            {
483                return ver;
484            }
485        }
486    
487       /**
488        * Test if the artifact scheme is recognized.  Specificially
489        * the test validates that the artifact scheme corresponding to 
490        * 'artifact', link', or 'local'.
491        * @return true if the uri scheme is recognized
492        */
493        public boolean isRecognized()
494        {
495            return isRecognized( m_uri );
496        }
497    
498        /**
499         * Create an artifact url backed by the repository.
500         *
501         * @return the artifact url
502         */
503        public URL toURL()
504        {
505            String scheme = getScheme();
506            if( ARTIFACT.equals( scheme ) )
507            {
508                return toURL( new net.dpml.transit.artifact.Handler() );
509            }
510            else if( LINK.equals( scheme ) )
511            {
512                return toURL( new net.dpml.transit.link.Handler() );
513            }
514            else if( LOCAL.equals( scheme ) )
515            {
516                return toURL( new net.dpml.transit.local.Handler() );
517            }
518            else
519            {
520                final String error = 
521                  "URI scheme not recognized: " + m_uri;
522                throw new UnsupportedSchemeException( error );
523            }
524        }
525    
526        /**
527         * Create an artifact url backed by the repository.
528         * @param handler the protocol handler
529         * @return the artifact url
530         */
531        public URL toURL( URLStreamHandler handler )
532        {
533            try
534            {
535                return new URL( null, m_uri.toASCIIString(), handler );
536            }
537            catch( MalformedURLException e )
538            {
539                // Can not happen!
540                final String error =
541                  "An artifact URI could not be converted to a URL [" 
542                  + m_uri 
543                  + "].";
544                throw new TransitRuntimeException( error );
545            }
546        }
547    
548        /**
549         * Create an artifact url backed by the repository.
550         *
551         * @return the artifact url
552         */
553        public URI toURI()
554        {
555            return m_uri;
556        }
557    
558        // ------------------------------------------------------------------------
559        // Comparable
560        // ------------------------------------------------------------------------
561    
562        /**
563         * Compare this artifact with another artifact.  Artifact comparisom is
564         * based on a comparison of the string representation of the artifact with
565         * the string representation of the supplied object.
566         *
567         * @param object the object to compare with this instance
568         * @return the comparative order of the supplied object relative to this
569         *   artifact
570         * @exception NullArgumentException if the supplied object argument is null.
571         * @exception ClassCastException if the supplied object is not an Artifact.
572         */
573        public int compareTo( Object object )
574            throws NullArgumentException, ClassCastException
575        {
576            if( object instanceof Artifact )
577            {
578                String name = this.toString();
579                return name.compareTo( object.toString() );
580            }
581            else if( null == object )
582            {
583                throw new NullArgumentException( "object" );
584            }
585            else
586            {
587                final String error =
588                  "Object ["
589                  + object.getClass().getName()
590                  + "] does not implement ["
591                  + this.getClass().getName() + "].";
592                throw new ClassCastException( error );
593            }
594        }
595    
596        // ------------------------------------------------------------------------
597        // Object
598        // ------------------------------------------------------------------------
599    
600        /**
601         * Return a string representation of the artifact.
602         * @return the artifact as a uri
603         */
604        public String toString()
605        {
606             return m_uri.toString();
607        }
608    
609        /**
610         * Compare this artifact with the supplied object for equality.  This method
611         * will return true if the supplied object is an Artifact and has an equal
612         * uri.
613         *
614         * @param other the object to compare with this instance
615         * @return TRUE if this artifact is equal to the supplied object
616         */
617        public boolean equals( Object other )
618        {
619            if( null == other )
620            {
621                return false;
622            }
623            else if( this == other )
624            {
625                return true;
626            }
627            else if( other instanceof Artifact )
628            {
629                Artifact art = (Artifact) other;
630                return m_uri.equals( art.m_uri );
631            }
632            else
633            {
634                return false;
635            }
636        }
637    
638       /**
639        * Return the hashcode for the artifact.
640        * @return the hashcode value
641        */
642        public int hashCode()
643        {
644            return m_uri.hashCode();
645        }
646    }
647